package com.gitee.urick.union.template;

import com.gitee.urick.union.config.UnionConfig;
import com.gitee.urick.union.constants.UnionTag;
import com.gitee.urick.union.model.HtmlParser;
import com.gitee.urick.union.model.UnionApiDocGroup;
import com.gitee.urick.union.utils.UnionDocUtil;
import com.power.common.util.CollectionUtil;
import com.power.common.util.StringUtil;
import com.power.common.util.ValidateUtil;
import com.power.doc.builder.ProjectDocConfigBuilder;
import com.power.doc.constants.DocAnnotationConstants;
import com.power.doc.constants.DocGlobalConstants;
import com.power.doc.constants.DocTags;
import com.power.doc.helper.ParamsBuildHelper;
import com.power.doc.model.*;
import com.power.doc.template.IDocBuildTemplate;
import com.power.doc.utils.*;
import com.thoughtworks.qdox.model.*;
import com.thoughtworks.qdox.model.impl.DefaultDocletTag;
import com.thoughtworks.qdox.model.impl.DefaultJavaParameterizedType;

import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;

import static com.power.doc.constants.DocTags.*;

/**
 * @author Rick 2021/02/08
 * @since 1.0
 */
public class UnionDocBuilderTemplate implements IDocBuildTemplate<ApiDoc> {

    /**
     * api index
     */
    private final AtomicInteger atomicInteger = new AtomicInteger(1);

    @Override
    public List<ApiDoc> getApiData(ProjectDocConfigBuilder projectBuilder) {
        UnionConfig apiConfig = (UnionConfig) projectBuilder.getApiConfig();
        List<ApiDoc> apiDocList = new ArrayList<>();
        boolean setCustomOrder = false;
        AtomicInteger order = new AtomicInteger(1);
        // build Api Doc
        Collection<JavaClass> handlers = projectBuilder.getJavaProjectBuilder().getClasses();
        if (apiConfig.isClassApiType()) {
            Map<String, UnionApiDocGroup> apiDocGroup = new HashMap<>();
            for (JavaClass cls : handlers) {
                if (Objects.nonNull(cls.getTagByName(IGNORE))) {
                    continue;
                }

                if (StringUtil.isNotEmpty(apiConfig.getPackageFilters())) {
                    if (!DocUtil.isMatch(apiConfig.getPackageFilters(), cls.getCanonicalName())) {
                        continue;
                    }
                }
                if (checkNonUnionInterface(cls, true)) continue;

                DocletTag groupTag = Optional.ofNullable(cls.getTagByName(GROUP)).orElse(new DefaultDocletTag("normal", "normal"));
                String groupMame = groupTag.getValue();
                if (!apiDocGroup.containsKey(groupMame)) {
                    UnionApiDocGroup group = groupMame.equals("normal") ? new UnionApiDocGroup(true) : new UnionApiDocGroup(false);
                    String strOrder = JavaClassUtil.getClassTagsValue(cls, DocTags.ORDER, Boolean.TRUE);
                    if (ValidateUtil.isNonnegativeInteger(strOrder)) {
                        order.set(Integer.parseInt(strOrder));
                        setCustomOrder = true;
                    }
                    group.setOrder(order.get());

                    if (!group.isDefaultGroup()) order.incrementAndGet();

                    group.setAlias(groupMame);
                    group.setDesc(groupMame);
                    group.setName(groupMame);
                    apiDocGroup.put(groupMame, group);
                }

                UnionApiDocGroup apiGroup = apiDocGroup.get(groupMame);
                if (Objects.isNull(apiGroup.getList())) apiGroup.setList(new ArrayList<>());
                List<ApiMethodDoc> list = apiGroup.getList();
                // builder java class doc
                list.add(buildClassJavaMethod(projectBuilder, apiConfig, cls, apiGroup, order));
            }
            // 一个类一个JavaMethodDoc
            apiDocList.addAll(apiDocGroup.values());
        } else {
            for (JavaClass cls : handlers) {
                if (Objects.nonNull(cls.getTagByName(IGNORE))) {
                    continue;
                }

                if (StringUtil.isNotEmpty(apiConfig.getPackageFilters())) {
                    if (!DocUtil.isMatch(apiConfig.getPackageFilters(), cls.getCanonicalName())) {
                        continue;
                    }
                }
                if (checkNonUnionInterface(cls, false)) continue;

                String strOrder = JavaClassUtil.getClassTagsValue(cls, DocTags.ORDER, Boolean.TRUE);
                if (ValidateUtil.isNonnegativeInteger(strOrder)) {
                    order.set(Integer.parseInt(strOrder));
                    setCustomOrder = true;
                }
                List<ApiMethodDoc> apiMethodDocs = buildJavaMethod(cls, apiConfig, projectBuilder);
                this.handleApiDoc(cls, apiDocList, apiMethodDocs, order, apiConfig.isMd5EncryptedHtmlName());
                order.incrementAndGet();
            }
        }

        // sort
        if (apiConfig.isSortByTitle()) {
            Collections.sort(apiDocList);
        }
        if (setCustomOrder) {
            // while set custom oder
            return apiDocList.stream()
                    .sorted(Comparator.comparing(ApiDoc::getOrder))
                    .peek(p -> p.setOrder(atomicInteger.getAndAdd(1))).collect(Collectors.toList());
        } else {
            return apiDocList.stream()
                    .sorted(Comparator.comparing(ApiDoc::getOrder))
                    .collect(Collectors.toList());
        }
    }

    void handleApiDoc(JavaClass cls, List<ApiDoc> apiDocList, List<ApiMethodDoc> apiMethodDocs, AtomicInteger order, boolean isUseMD5) {
        String controllerName = cls.getName();
        UnionApiDocGroup apiDoc = new UnionApiDocGroup(false);
        apiDoc.setOrder(order.get());
        apiDoc.setName(controllerName);
        apiDoc.setAlias(controllerName);
        if (isUseMD5) {
            String name = DocUtil.generateId(apiDoc.getName());
            apiDoc.setAlias(name);
        }
        String desc = DocUtil.getEscapeAndCleanComment(cls.getComment());
        apiDoc.setDesc(desc);
        apiDoc.setList(apiMethodDocs);
        apiDocList.add(apiDoc);
    }

    public ApiDoc getSingleApiData(ProjectDocConfigBuilder projectBuilder, String apiClassName) {
        return null;
    }

    public boolean ignoreReturnObject(String typeName, List<String> ignoreParams) {
        return false;
    }

    /**
     * 构建数据
     *
     * @param cls
     * @param apiConfig
     * @param projectBuilder
     * @return
     */
    private List<ApiMethodDoc> buildJavaMethod(final JavaClass cls, UnionConfig apiConfig, ProjectDocConfigBuilder projectBuilder) {
        String clazName = cls.getCanonicalName();
        List<JavaMethod> methods = cls.getMethods();
        List<ApiMethodDoc> methodDocList = new ArrayList<>(methods.size());
        int methodOrder = 0;
        for (JavaMethod method : methods) {
            if (method.isPrivate()) {
                continue;
            }
            if (StringUtil.isEmpty(method.getComment()) && apiConfig.isStrict()) {
                throw new RuntimeException("Unable to find comment for method " + method.getName() + " in " + cls.getCanonicalName());
            }
            boolean deprecated = false;
            //Deprecated
            List<JavaAnnotation> annotations = method.getAnnotations();
            for (JavaAnnotation annotation : annotations) {
                String annotationName = annotation.getType().getName();
                if (DocAnnotationConstants.DEPRECATED.equals(annotationName)) {
                    deprecated = true;
                }
            }
            if (Objects.nonNull(method.getTagByName(DEPRECATED))) {
                deprecated = true;
            }
            methodOrder++;
            ApiMethodDoc apiMethodDoc = new ApiMethodDoc();
            apiMethodDoc.setOrder(methodOrder);
            apiMethodDoc.setDesc(DocUtil.getEscapeAndCleanComment(method.getComment()));
            apiMethodDoc.setName(method.getName());
            String methodUid = DocUtil.generateId(clazName + method.getName());
            apiMethodDoc.setMethodId(methodUid);
            String apiNoteValue = DocUtil.getNormalTagComments(method, DocTags.API_NOTE, cls.getName());
            if (StringUtil.isEmpty(apiNoteValue)) {
                apiNoteValue = method.getComment();
            }

            if (apiConfig.isShowAuthor()) {
                String authorText = JavaClassUtil.getClassTagsValue(cls, DocTags.AUTHOR, Boolean.TRUE);
               if(StringUtil.isEmpty(authorText)) apiMethodDoc.setAuthor(authorText);
            }

            String type = UnionDocUtil.getNormalTagComments(method, UnionTag.TYPE, cls.getName());
            if (StringUtil.isNotEmpty(type)) {
                apiMethodDoc.setType(type);
            }

            String headers = UnionDocUtil.getHeaderComments(method, UnionTag.HEADER);
            if (StringUtil.isNotEmpty(headers)) {
                if (apiConfig.isHtml()) {
                    headers = "Header | Type|Description|Required|Since\n---|---|---|---|----\n" + headers;
                    HtmlParser tocParser = new HtmlParser(false);
                    headers = tocParser.parserHtml(headers);
                }
                apiMethodDoc.setHeaders(headers);
            }

            apiMethodDoc.setDetail(apiNoteValue);
            if (Objects.nonNull(method.getTagByName(IGNORE))) {
                continue;
            }
            apiMethodDoc.setDeprecated(deprecated);
            // build request params
            List<ApiParam> requestParams = requestParams(method, projectBuilder);
            apiMethodDoc.setRequestParams(requestParams);
            // build response params
            List<ApiParam> responseParams = buildReturnApiParams(DocJavaMethod.builder().setJavaMethod(method), projectBuilder);
            apiMethodDoc.setResponseParams(responseParams);
            methodDocList.add(apiMethodDoc);

        }
        return methodDocList;
    }

    /**
     * @param projectBuilder
     * @param apiConfig
     * @param cls
     * @param unionApiDoc
     * @return
     */
    private ApiMethodDoc buildClassJavaMethod(ProjectDocConfigBuilder projectBuilder, UnionConfig apiConfig, JavaClass cls, UnionApiDocGroup unionApiDoc, AtomicInteger order) {
        String clazzName = cls.getCanonicalName();
        if (StringUtil.isEmpty(cls.getComment()) && apiConfig.isStrict()) {
            throw new RuntimeException("Unable to find comment for class " + cls.getName() + " in " + clazzName);
        }
        boolean deprecated = false;
        //Deprecated
        List<JavaAnnotation> annotations = cls.getAnnotations();
        for (JavaAnnotation annotation : annotations) {
            String annotationName = annotation.getType().getName();
            if (DocAnnotationConstants.DEPRECATED.equals(annotationName)) {
                deprecated = true;
            }
        }
        if (Objects.nonNull(cls.getTagByName(DEPRECATED))) {
            deprecated = true;
        }

        ApiMethodDoc apiMethodDoc = new ApiMethodDoc();
        apiMethodDoc.setMethodId(DocUtil.generateId(clazzName));//基于方法名生成
        apiMethodDoc.setName(cls.getName());
        apiMethodDoc.setDeprecated(deprecated);
        if (unionApiDoc.isDefaultGroup()) {
            apiMethodDoc.setOrder(order.get());
            order.incrementAndGet();
        } else {
            unionApiDoc.setSubOrder(unionApiDoc.getSubOrder() + 1);
            apiMethodDoc.setOrder(unionApiDoc.getSubOrder());
        }

        apiMethodDoc.setDesc(UnionDocUtil.getEscapeAndCleanComment(cls.getComment()));
        String apiNoteValue = UnionDocUtil.getNormalTagComments(cls, DocTags.API_NOTE, cls.getName());
        if (StringUtil.isEmpty(apiNoteValue)) {
            apiNoteValue = cls.getComment();
        }
        apiMethodDoc.setDetail(apiNoteValue);

        String type = UnionDocUtil.getNormalTagComments(cls, UnionTag.TYPE, cls.getName());
        if (StringUtil.isNotEmpty(type)) {
            apiMethodDoc.setType(type);
        }

        if (apiConfig.isShowAuthor()) {
            String authorText = JavaClassUtil.getClassTagsValue(cls, DocTags.AUTHOR, Boolean.TRUE);
            if(StringUtil.isNotEmpty(authorText)) apiMethodDoc.setAuthor(authorText);
        }

        String headers = UnionDocUtil.getHeaderComments(cls, UnionTag.HEADER);
        if (StringUtil.isNotEmpty(headers)) {
            if (apiConfig.isHtml()) {
                headers = "Header | Type|Description|Required|Since\n---|---|---|---|----\n" + headers;
                HtmlParser tocParser = new HtmlParser(false);
                headers = tocParser.parserHtml(headers);
            }
            apiMethodDoc.setHeaders(headers);
        }

        DefaultJavaParameterizedType reqAndRespType = (DefaultJavaParameterizedType) cls.getSuperClass();
        List<JavaType> actualTypeArguments = reqAndRespType.getActualTypeArguments();
        if (CollectionUtil.isNotEmpty(actualTypeArguments)) {
            if (actualTypeArguments.size() != 2) {//class handler param must second
                throw new RuntimeException("This is actual type arguments must second  for class " + cls.getName() + " in " + clazzName);
            }
            // build request params
            JavaClass reqClass = projectBuilder.getJavaProjectBuilder().getClassByName(actualTypeArguments.get(0).getFullyQualifiedName());
            boolean paramsDataToTree = projectBuilder.getApiConfig().isParamsDataToTree();
            List<ApiParam> reqParams = requestParams(reqClass, projectBuilder);
            if (paramsDataToTree) {
                reqParams = ApiParamTreeUtil.apiParamToTree(reqParams);
            }
            apiMethodDoc.setRequestParams(reqParams);

            // build response params
            JavaClass respClass = projectBuilder.getJavaProjectBuilder().getClassByName(actualTypeArguments.get(1).getFullyQualifiedName());
            List<ApiParam> respParams = requestParams(respClass, projectBuilder);
            if (paramsDataToTree) {
                respParams = ApiParamTreeUtil.apiParamToTree(respParams);
            }
            apiMethodDoc.setResponseParams(respParams);
        }
        return apiMethodDoc;
    }

    private List<ApiParam> requestParams(final JavaMethod javaMethod, ProjectDocConfigBuilder builder) {
        boolean isStrict = builder.getApiConfig().isStrict();
        boolean isShowJavaType = builder.getApiConfig().getShowJavaType();
        Map<String, CustomRespField> responseFieldMap = new HashMap<>();
        String className = javaMethod.getDeclaringClass().getCanonicalName();
        Map<String, String> paramTagMap = DocUtil.getParamsComments(javaMethod, DocTags.PARAM, className);
        List<JavaParameter> parameterList = javaMethod.getParameters();
        if (parameterList.size() < 1) {
            return null;
        }
        List<ApiParam> paramList = new ArrayList<>();
        for (JavaParameter parameter : parameterList) {
            String paramName = parameter.getName();
            String typeName = parameter.getType().getGenericCanonicalName();
            String simpleName = parameter.getType().getValue().toLowerCase();
            String fullTypeName = parameter.getType().getFullyQualifiedName();
            if (!paramTagMap.containsKey(paramName) && JavaClassValidateUtil.isPrimitive(fullTypeName) && isStrict) {
                throw new RuntimeException("ERROR: Unable to find javadoc @param for actual param \""
                        + paramName + "\" in method " + javaMethod.getName() + " from " + className);
            }
            String comment = this.paramCommentResolve(paramTagMap.get(paramName));
            JavaClass javaClass = builder.getJavaProjectBuilder().getClassByName(fullTypeName);
            List<JavaAnnotation> annotations = parameter.getAnnotations();
            List<String> groupClasses = JavaClassUtil.getParamGroupJavaClass(annotations);
            if (JavaClassValidateUtil.isCollection(fullTypeName) || JavaClassValidateUtil.isArray(fullTypeName)) {
                if (JavaClassValidateUtil.isCollection(typeName)) {
                    typeName = typeName + "<T>";
                }
                String[] gicNameArr = DocClassUtil.getSimpleGicName(typeName);
                String gicName = gicNameArr[0];
                if (JavaClassValidateUtil.isArray(gicName)) {
                    gicName = gicName.substring(0, gicName.indexOf("["));
                }
                if (JavaClassValidateUtil.isPrimitive(gicName)) {
                    String processedType = isShowJavaType ?
                            JavaClassUtil.getClassSimpleName(typeName) : DocClassUtil.processTypeNameForParams(simpleName);
                    ApiParam param = ApiParam.of().setField(paramName)
                            .setType(processedType);
                    paramList.add(param);
                } else {
                    paramList.addAll(ParamsBuildHelper.buildParams(gicNameArr[0], DocGlobalConstants.EMPTY, 0, "true", responseFieldMap, Boolean.FALSE, new HashMap<>(), builder, groupClasses, 0));
                }
            } else if (JavaClassValidateUtil.isPrimitive(fullTypeName)) {
                ApiParam param = ApiParam.of().setField(paramName)
                        .setType(JavaClassUtil.getClassSimpleName(typeName))
                        .setDesc(comment).setRequired(true).setVersion(DocGlobalConstants.DEFAULT_VERSION);
                paramList.add(param);
            } else if (JavaClassValidateUtil.isMap(fullTypeName)) {
                if (JavaClassValidateUtil.isMap(typeName)) {
                    ApiParam apiParam = ApiParam.of().setField(paramName).setType(typeName)
                            .setDesc(comment).setRequired(true).setVersion(DocGlobalConstants.DEFAULT_VERSION);
                    paramList.add(apiParam);
                    continue;
                }
                String[] gicNameArr = DocClassUtil.getSimpleGicName(typeName);
                paramList.addAll(ParamsBuildHelper.buildParams(gicNameArr[1], DocGlobalConstants.EMPTY, 0, "true", responseFieldMap, Boolean.FALSE, new HashMap<>(), builder, groupClasses, 0));
            } else if (javaClass.isEnum()) {
                ApiParam param = ApiParam.of().setField(paramName)
                        .setType("Enum").setDesc(comment).setRequired(true).setVersion(DocGlobalConstants.DEFAULT_VERSION);
                paramList.add(param);
            } else {
                paramList.addAll(ParamsBuildHelper.buildParams(typeName, DocGlobalConstants.EMPTY, 0, "true", responseFieldMap, Boolean.FALSE, new HashMap<>(), builder, groupClasses, 0));
            }
        }
        return paramList;
    }

    private List<ApiParam> requestParams(final JavaClass javaClazz, ProjectDocConfigBuilder builder) {
        boolean isStrict = builder.getApiConfig().isStrict();
        boolean isShowJavaType = builder.getApiConfig().getShowJavaType();
        Map<String, CustomRespField> responseFieldMap = new HashMap<>();
        String className = javaClazz.getCanonicalName();
        Map<String, String> paramTagMap = UnionDocUtil.getParamsComments(javaClazz, DocTags.PARAM, className);
        List<ApiParam> paramList = new ArrayList<>();
        String paramName = javaClazz.getName();
        String typeName = javaClazz.getGenericCanonicalName();
        String simpleName = javaClazz.getValue().toLowerCase();
        String fullTypeName = javaClazz.getFullyQualifiedName();
        if (!paramTagMap.containsKey(paramName) && JavaClassValidateUtil.isPrimitive(fullTypeName) && isStrict) {
            throw new RuntimeException("ERROR: Unable to find javadoc @param for actual param \""
                    + paramName + "\" in Class " + className);
        }
        String comment = this.paramCommentResolve(paramTagMap.get(paramName));
        List<JavaAnnotation> annotations = javaClazz.getAnnotations();
        List<String> groupClasses = JavaClassUtil.getParamGroupJavaClass(annotations);
        if (JavaClassValidateUtil.isCollection(fullTypeName) || JavaClassValidateUtil.isArray(fullTypeName)) {
            if (JavaClassValidateUtil.isCollection(typeName)) {
                typeName = typeName + "<T>";
            }
            String[] gicNameArr = DocClassUtil.getSimpleGicName(typeName);
            String gicName = gicNameArr[0];
            if (JavaClassValidateUtil.isArray(gicName)) {
                gicName = gicName.substring(0, gicName.indexOf("["));
            }
            if (JavaClassValidateUtil.isPrimitive(gicName)) {
                String processedType = isShowJavaType ?
                        JavaClassUtil.getClassSimpleName(typeName) : DocClassUtil.processTypeNameForParams(simpleName);
                ApiParam param = ApiParam.of().setField(paramName)
                        .setType(processedType);
                paramList.add(param);
            } else {
                paramList.addAll(ParamsBuildHelper.buildParams(gicNameArr[0], DocGlobalConstants.EMPTY, 0, "true", responseFieldMap, Boolean.FALSE, new HashMap<>(), builder, groupClasses, 0));
            }
        } else if (JavaClassValidateUtil.isPrimitive(fullTypeName)) {
            ApiParam param = ApiParam.of().setField(paramName)
                    .setType(JavaClassUtil.getClassSimpleName(typeName))
                    .setDesc(comment).setRequired(true).setVersion(DocGlobalConstants.DEFAULT_VERSION);
            paramList.add(param);
        } else if (JavaClassValidateUtil.isMap(fullTypeName)) {
            if (JavaClassValidateUtil.isMap(typeName)) {
                ApiParam apiParam = ApiParam.of().setField(paramName).setType(typeName)
                        .setDesc(comment).setRequired(true).setVersion(DocGlobalConstants.DEFAULT_VERSION);
                paramList.add(apiParam);
            }
            String[] gicNameArr = DocClassUtil.getSimpleGicName(typeName);
            paramList.addAll(ParamsBuildHelper.buildParams(gicNameArr[1], DocGlobalConstants.EMPTY, 0, "true", responseFieldMap, Boolean.FALSE, new HashMap<>(), builder, groupClasses, 0));
        } else if (javaClazz.isEnum()) {
            ApiParam param = ApiParam.of().setField(paramName)
                    .setType("Enum").setDesc(comment).setRequired(true).setVersion(DocGlobalConstants.DEFAULT_VERSION);
            paramList.add(param);
        } else {
            paramList.addAll(ParamsBuildHelper.buildParams(typeName, DocGlobalConstants.EMPTY, 0, "true", responseFieldMap, Boolean.FALSE, new HashMap<>(), builder, groupClasses, 0));
        }
        return paramList;
    }

    /**
     * @param cls
     * @param isClassType 是否是class 类型接口，class 类型接口必须要有父类
     * @return
     */
    private boolean checkNonUnionInterface(JavaClass cls, boolean isClassType) {
        if (isClassType) {
            JavaType superClass = cls.getSuperClass();
            if (Objects.isNull(superClass) || Objects.equals(superClass.getCanonicalName(), "java.lang.Object")) {
                return true;
            }
        }
        List<DocletTag> docletTags = cls.getTags();
        for (DocletTag docletTag : docletTags) {
            String value = docletTag.getName();
            if (UnionTag.API_DOC.equals(value)) {
                return false;
            }
        }
        return true;
    }
}
